﻿using Dijiang.Errors;
using Dijiang.Values;
using System;

namespace Dijiang
{
    public static class BuiltinFunctions
    {
        public static void Load(CmdEnv env)
        {
            env.Put("+", new DBuiltin(OpPlus));
            env.Put("-", new DBuiltin(OpMinus));
            env.Put("*", new DBuiltin(OpMultiply));
            env.Put("/", new DBuiltin(OpDivide));
            env.Put("=", new DBuiltin(OpEquals));
            env.Put(">", new DBuiltin(OpGT));
            env.Put("<", new DBuiltin(OpLT));
            env.Put(">=", new DBuiltin(OpGE));
            env.Put("<=", new DBuiltin(OpLE));
            env.Put("not", new DBuiltin(OpNot));

            env.Put("eval", new DBuiltin(Eval));
            env.Put("bind", new DBuiltin(Bind));
            env.Put("lambda", new DBuiltin(CreateClosure));
            env.Put("cond", new DBuiltin(Cond));
            env.Put("if", new DBuiltin(If));
            env.Put("exe-seq", new DBuiltin(Seqential));

            env.Put("sqrt", new DBuiltin(Sqrt));

            env.Put("list", new DBuiltin(CreateList));
            env.Put("head", new DBuiltin(Head));
            env.Put("tail", new DBuiltin(Tail));
            env.Put("nil?", new DBuiltin(IsNil));
            env.Put("join", new DBuiltin(Concat));
            env.Put("take", new DBuiltin(Take));
            env.Put("nth", new DBuiltin(Nth));
            env.Put("pop", new DBuiltin(Pop));
            env.Put("len", new DBuiltin(Len));

            env.Put("log-env", new DBuiltin(LogEnv));
        }

        public static DValue IsNil(CmdEnv env, DSequence args)
        {
            return new DBoolean(args.Pop() is DSequence s && s.Count == 0);
        }

        #region Operators
        public static DValue OpPlus(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DNumber x) return new WrongTypeError(typeof(DNumber));

            while (args.Count > 0)
            {
                if (args.Pop() is not DNumber y) return new WrongTypeError(typeof(DNumber));

                x.num += y.num;
            }

            return x;
        }

        public static DValue OpMinus(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DNumber x) return new WrongTypeError(typeof(DNumber));

            if (args.Count == 0)
            {
                x.num = -x.num;
                return x;
            }

            while (args.Count > 0)
            {
                if (args.Pop() is not DNumber y) return new WrongTypeError(typeof(DNumber));

                x.num -= y.num;
            }

            return x;
        }

        public static DValue OpMultiply(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DNumber x) return new WrongTypeError(typeof(DNumber));
            while (args.Count > 0)
            {
                if (args.Pop() is not DNumber y) return new WrongTypeError(typeof(DNumber));

                x.num *= y.num;
            }

            return x;
        }

        public static DValue OpDivide(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DNumber x) return new WrongTypeError(typeof(DNumber));

            while (args.Count > 0)
            {
                if (args.Pop() is not DNumber y) return new WrongTypeError(typeof(DNumber));

                if (y.num.IsZero()) return new DivideByZeroError();

                x.num /= y.num;
            }

            return x;
        }

        public static DValue OpEquals(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DNumber x) return new WrongTypeError(typeof(DNumber));
            while (args.Count > 0)
            {
                if (args.Pop() is not DNumber y) return new WrongTypeError(typeof(DNumber));

                if (!x.Equals(y))
                {
                    return DBoolean.FALSE;
                }
            }

            return DBoolean.TRUE;
        }

        public static DValue OpNot(CmdEnv env, DSequence args)
        {
            if (args.Count != 1) return new WrongArgumentsError(args.Count, 1);

            if (args.Pop() is not DBoolean b) return new WrongTypeError(typeof(DBoolean));

            b.value = !b.value;

            return b;
        }

        public static DValue OpGT(CmdEnv env, DSequence args)
        {
            if (args.Count != 2) return new WrongArgumentsError(args.Count, 2);

            if (args.Pop() is not DNumber lhs) return new WrongTypeError(typeof(DNumber));
            if (args.Pop() is not DNumber rhs) return new WrongTypeError(typeof(DNumber));

            return new DBoolean(lhs.num > rhs.num);
        }

        public static DValue OpLT(CmdEnv env, DSequence args)
        {
            if (args.Count != 2) return new WrongArgumentsError(args.Count, 2);

            if (args.Pop() is not DNumber lhs) return new WrongTypeError(typeof(DNumber));
            if (args.Pop() is not DNumber rhs) return new WrongTypeError(typeof(DNumber));

            return new DBoolean(lhs.num < rhs.num);
        }

        public static DValue OpGE(CmdEnv env, DSequence args)
        {
            if (args.Count != 2) return new WrongArgumentsError(args.Count, 2);

            if (args.Pop() is not DNumber lhs) return new WrongTypeError(typeof(DNumber));
            if (args.Pop() is not DNumber rhs) return new WrongTypeError(typeof(DNumber));

            return new DBoolean(lhs.num > rhs.num || lhs.num.Equals(rhs.num));
        }

        public static DValue OpLE(CmdEnv env, DSequence args)
        {
            if (args.Count != 2) return new WrongArgumentsError(args.Count, 2);

            if (args.Pop() is not DNumber lhs) return new WrongTypeError(typeof(DNumber));
            if (args.Pop() is not DNumber rhs) return new WrongTypeError(typeof(DNumber));

            return new DBoolean(lhs.num < rhs.num || lhs.num.Equals(rhs.num));
        }
        #endregion

        #region Basics
        public static DValue Eval(CmdEnv env, DSequence args)
        {
            if (args.Count != 1) return new WrongArgumentsError(args.Count, 1);

            return env.Evaluate(args.Pop());
        }

        public static DValue Bind(CmdEnv env, DSequence args)
        {
            while (env.parent != null) env = env.parent;

            if (args.Count != 2) return new WrongArgumentsError(args.Count, 2);

            switch (args.Pop())
            {
                case DSequence symbols:
                    if (symbols.Count != args.Count) return new DError($"attempt to bind {symbols.Count} symbols to {args.Count} definitions.");

                    var result = new DList();

                    while (symbols.Count > 0)
                    {
                        var val = env.Put(symbols.Pop(), args.Pop());

                        if (val is DError err) return err;

                        result.Add(val);
                    }

                    if (result.Count == 1) return result.Pop();

                    return result;
                case DSymbol symbol:
                    if (args.Count == 0) return new DError($"No value bound to {symbol.sym}");
                    return env.Put(symbol, args.Pop());

                default:
                    return new DError("Trying to bind value(s) to non-symbol");
            }
        }

        public static DValue CreateClosure(CmdEnv env, DSequence args)
        {
            var formals = args.Pop();
            var body = args.Pop();

            // TODO : throw errors.

            return new DClosure(formals, body);
        }

        public static DValue If(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DBoolean judge) return new WrongTypeError(typeof(DBoolean));

            if (judge.value)
            {
                var thenBody = args.Pop(0);

                return env.Evaluate(thenBody);
            }
            else
            {
                var elseBody = args.Pop(1);

                return env.Evaluate(elseBody);
            }
        }

        public static DValue Cond(CmdEnv env, DSequence args)
        {
            while (args.Count > 0)
            {
                if (args.Pop() is not DList branch) return new WrongTypeError(typeof(DList));

                var fst = branch.Pop();
                if (fst is DSymbol end && end.sym == "else")
                {
                    if (args.Count > 0)
                    {
                        return new DError("else branch must at last");
                    }
                    else
                    {
                        return env.Evaluate(branch.Pop());
                    }
                }

                if (fst.Eval(env) is not DBoolean judge) return new WrongTypeError(typeof(DBoolean));

                if (judge.value) return env.Evaluate(branch.Pop());
            }

            return new DError("function cond must have a default branch [else [..]]");
        }

        // eval each expressions and return last expression.
        public static DValue Seqential(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DList experssions) return new WrongTypeError(typeof(DList));

            while (experssions.Count > 1)
            {
                experssions.Pop().Eval(env);
            }

            return experssions.Pop().Eval(env);
        }

        #endregion

        #region Math
        public static DValue Sqrt(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DNumber v) return new WrongTypeError(typeof(DNumber));

            return new DNumber(MathF.Sqrt(v.FloatVal));
        }
        #endregion

        #region List Methods
        public static DValue CreateList(CmdEnv env, DSequence args)
        {
            var list = new DList();

            while (args.Count > 0)
            {
                list.Add(args.Pop());
            }

            return list;
        }

        public static DValue Head(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DSequence list) return new WrongTypeError(typeof(DSequence));

            return list.Pop();
        }

        public static DValue Tail(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DSequence list) return new WrongTypeError(typeof(DSequence));

            list.Pop();

            return list;
        }

        public static DValue Take(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DNumber count) return new WrongTypeError(typeof(DNumber));
            if (args.Pop() is not DSequence list) return new WrongTypeError(typeof(DSequence));

            var cnt = count.IntVal;
            var result = new DList();
            while (cnt > 0)
            {
                result.Add(list.Pop());
                cnt--;
            }
            return result;
        }

        public static DValue Nth(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DNumber index) return new WrongTypeError(typeof(DNumber));
            if (args.Pop() is not DSequence list) return new WrongTypeError(typeof(DSequence));

            var idx = index.IntVal;

            if (idx < 0 || idx >= list.Count) return new DError($"Out of index. expect [0, {list.Count}), got {idx}");

            return list.items[idx];
        }

        public static DValue Pop(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DNumber index) return new WrongTypeError(typeof(DNumber));
            if (args.Pop() is not DSequence list) return new WrongTypeError(typeof(DSequence));

            var idx = index.IntVal;

            if (idx < 0 || idx >= list.Count) return new DError($"Out of index. expect [0, {list.Count}), got {idx}");

            return list.Pop(idx);
        }

        public static DValue Concat(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DSequence result) return new WrongTypeError(typeof(DSequence));
            while (args.Count > 0)
            {
                if (args.Pop() is not DSequence list) return new WrongTypeError(typeof(DSequence));

                result.Concat(list);
            }
            return result;
        }

        public static DValue Len(CmdEnv env, DSequence args)
        {
            if (args.Pop() is not DSequence list) return new WrongTypeError(typeof(DSequence));
            return new DNumber(list.Count);
        }
        #endregion

        public static DValue LogEnv(CmdEnv env, DSequence args)
        {
            foreach (var (name, value) in env.variables)
            {
                Console.WriteLine($"({name}, {value})");
            }
            return new DList();
        }
    }
}
